﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Reflection;

namespace WMemoryProfiler
{

    /// <summary>
    /// Helper methods to find the debugger executable, path to sos.dll and a check if a process is 64 bit or not.
    /// </summary>
    public class ProcessInfos
    {
        /// <summary>
        /// Process id of current process
        /// </summary>
        public static int SelfPid = Process.GetCurrentProcess().Id;

        /// <summary>
        /// Check if process is a 64 bit process
        /// </summary>
        /// <param name="pid">process id to check</param>
        /// <returns>true if process is 64 bit, false otherwise.</returns>
        private static bool IsWin64Process(int pid)
        {
            if( Directory.Exists("C:\\Program Files (x86)") ) // 64 bit Windows 
            {
                IntPtr processHandle;
                bool retVal;

                try
                {
                    using (var p = Process.GetProcessById(pid))
                    {
                        processHandle = p.Handle;
                        if (NativeMethods.IsWow64Process(processHandle, out retVal))
                        {
                            retVal = !retVal;
                        }
                    }
                }
                catch
                {
                    return false; // access is denied to the process
                }

                return retVal;
            }

            return false; // not on 64-bit Windows
        }

        /// <summary>
        /// Get the path to cdb.exe (x86,x64) 
        /// </summary>
        /// <param name="pid">Process id to debug</param>
        /// <returns>Full path to correct cdb.exe</returns>
        /// <exception cref="FileNotFoundException">No matching debugger could be found.</exception>
        public static string GetMatchingCdb(int pid)
        {
            // check if process is still alive.
            using (var p = Process.GetProcessById(pid))
            {
                if (p.HasExited)
                {
                    throw new NotSupportedException(String.Format("The process with id {0} has already exited", p.Id));
                }
            }

            string cdb = LocateDebugger(IsWin64Process(pid));

            if( !File.Exists(cdb) )
            {
                Process.Start("https://dev.windows.com/en-us/downloads/windows-10-sdk");
                throw new FileNotFoundException(
                    String.Format("Could not locate windbg at default install location at: {0}. Please install Windbg first." + Environment.NewLine +
                                  "1. Download the SDK installer and start it" + Environment.NewLine +
                                  "2. Click Continue/Yes until you reach the screen where you can select what components you want to download" + Environment.NewLine +
                                  "3. Uncheck all check boxes and check only \"Debugging Tools for Windows\""+ Environment.NewLine+
                                  "4. Install it"+ Environment.NewLine +
                                  "7. You should have now under C:\\Program Files (x86)\\Windows Kits\\8.1\\Debuggers\\x86 and C:\\Program Files\\Windows Kits\\8.1\\Debuggers\\x64 the 32 and 64 bit version of Windbg installed."
                                  , cdb));
                                  
            }

            return cdb;
        }

        private static string LocateDebugger(bool bSearch64BitDebugger)
        {
            string cdb = LocateDebuggerAtAssemblyLocation(bSearch64BitDebugger);
            string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
            string programFilesx86 = Path.Combine(Path.GetDirectoryName(programFiles), "Program files (x86)");

            if (cdb == null)
            {
                if (bSearch64BitDebugger)
                {
                    if (!File.Exists(cdb)) // try SDK path which is in Program Files located starting with Windows 10 SDK for x64 Debugger
                    {
                        cdb = Path.Combine(Path.Combine(programFiles, "Windows Kits\\10\\Debuggers\\x64"), "cdb.exe");
                    }

                    if (!File.Exists(cdb)) // try SDK path which is in Program Files (x86) located
                    {
                        cdb = Path.Combine(Path.Combine(programFilesx86, "Windows Kits\\8.1\\Debuggers\\x64"), "cdb.exe");
                    }

                    if (!File.Exists(cdb)) // try SDK path which is in Program Files (x86) located
                    {
                        cdb = Path.Combine(Path.Combine(programFilesx86, "Windows Kits\\8.0\\Debuggers\\x64"), "cdb.exe");
                    }

                }
                else // get 32 bit debguger
                {
                    if (Directory.Exists(programFilesx86)) // if this directory exists assume a 64 bit Windows
                    {
                        programFiles = programFilesx86;
                    }

                    if (!File.Exists(cdb))
                    {
                        cdb = Path.Combine(Path.Combine(programFiles, "Windows Kits\\10\\Debuggers\\x86"), "cdb.exe");
                    }

                    if (!File.Exists(cdb))
                    {
                        cdb = Path.Combine(Path.Combine(programFiles, "Windows Kits\\8.1\\Debuggers\\x86"), "cdb.exe");
                    }
                    if (!File.Exists(cdb))
                    {
                        cdb = Path.Combine(Path.Combine(programFiles, "Windows Kits\\8.0\\Debuggers\\x86"), "cdb.exe");
                    }
                }
            }

            return cdb; 
        }

        private static string LocateDebuggerAtAssemblyLocation(bool bSearch64BitDebugger)
        {
            // try to locate a matching windbg of right bitness besides the location where WMemoryProfiler.dll is located
            // if one is found prefer that version of Windbg over the locally installed ones.
            string sidebySideCdb = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "cdb.exe");

            return LocateDebugger(sidebySideCdb, bSearch64BitDebugger);
        }

        internal static string LocateDebugger(string pathToDebugger, bool bSearch64BitDebugger)
        {
            string lret = null;

            CorFlagsReader flagReader = null;

            if (File.Exists(pathToDebugger))
            {
                flagReader = CorFlagsReader.ReadAssemblyMetadata(pathToDebugger);
                ProcessorArchitecture requested = bSearch64BitDebugger ? ProcessorArchitecture.Amd64 : ProcessorArchitecture.X86;
                lret = flagReader.ProcessorArchitecture == requested ? pathToDebugger : null;
            }

            return lret;


        }


        /// <summary>
        /// Get path to sos.dll of a started managed process. 
        /// </summary>
        /// <param name="process">Managed process instance</param>
        /// <returns>Path to sos.dll or null if the process was unmanaged or the CLR did not start up after 10s.</returns>
        public static string GetSosPath(Process process)
        {
            int modCount = 0;
            // wait 20s until the CLR has been started or not ... to find out which one was loaded
            for(int i=0;i<20;i++)
            {
                foreach (var mod in process.Modules.Cast<ProcessModule>())
                {
                    process.Refresh();
                    string file = Path.GetFileName(mod.FileName).ToLowerInvariant();
                    if (file == "clr.dll" ||
                        file == "mscorwks.dll")
                    {
                        return Path.Combine(Path.GetDirectoryName(mod.FileName), "sos.dll");
                    }
                }

                modCount = process.Modules.Count;
                Thread.Sleep(1000);
            }

            return null;
        }

    }
}
